<?php

namespace app\common\util;

use Predis\Client as RedisClient;

class Snowflake
{
    const EPOCH = 1543223810238; // 起始时间戳，毫秒

    const SEQUENCE_BITS = 12; // 序号部分12位
    const SEQUENCE_MAX = -1 ^ (-1 << self::SEQUENCE_BITS); // 序号最大值

    const WORKER_BITS = 10; // 节点部分10位
    const WORKER_MAX = -1 ^ (-1 << self::WORKER_BITS); // 节点最大数值

    const TIME_SHIFT = self::WORKER_BITS + self::SEQUENCE_BITS; // 时间戳部分左偏移量
    const WORKER_SHIFT = self::SEQUENCE_BITS; // 节点部分左偏移量

    protected int $timestamp; // 上次ID生成时间戳
    protected $workerId; // 节点ID
    protected int $sequence; // 序号
    protected $redis; // Redis 客户端实例

    public function __construct($workerId, $redisClient)
    {
        if ($workerId < 0 || $workerId > self::WORKER_MAX) {
            throw new \InvalidArgumentException("Worker ID 超出范围");
        }

        $this->timestamp = 0;
        $this->workerId = $workerId;
        $this->sequence = 0;
        $this->redis = $redisClient;
    }

    /**
     * 生成ID
     * @return int
     */
    public function getId(): int
    {
        $lockKey = 'snowflake_lock';
        $lockAcquired = $this->acquireLock($lockKey);

        if (!$lockAcquired) {
            throw new \RuntimeException("Failed to acquire lock for generating ID.");
        }

        try {
            $now = $this->now();
            if ($this->timestamp == $now) {
                $this->sequence++;
                if ($this->sequence > self::SEQUENCE_MAX) {
                    // 当前毫秒内生成的序号已经超出最大范围，等待下一毫秒重新生成
                    while ($now <= $this->timestamp) {
                        $now = $this->now();
                    }
                }
            } else {
                $this->sequence = 0;
            }

            $this->timestamp = $now; // 更新ID生时间戳

            return (($now - self::EPOCH) << self::TIME_SHIFT) | ($this->workerId << self::WORKER_SHIFT) | $this->sequence;
        } finally {
            $this->releaseLock($lockKey);
        }
    }

    /**
     * 获取当前毫秒
     * @return int
     */
    private function now(): int
    {
        return (int)(microtime(true) * 1000);
    }

    /**
     * 尝试获取锁
     *
     * @param string $key
     * @param int $timeoutMs
     * @return bool
     */
    private function acquireLock(string $key, int $timeoutMs = 1000): bool
    {
        $endTime = microtime(true) * 1000 + $timeoutMs;
        do {
            $isLocked = $this->redis->setnx($key, 1); // SETNX 命令是原子性的
            if ($isLocked) {
                $this->redis->expire($key, 1); // 设置锁的过期时间为1秒，防止死锁
                return true;
            }
            usleep(100); // 等待100微秒后重试
        } while (microtime(true) * 1000 < $endTime);

        return false;
    }

    /**
     * 释放锁
     * @param string $key 锁的键名
     */
    private function releaseLock(string $key): void
    {
        $this->redis->del($key);
    }
}